﻿//////////////////////////////////////////////////////////////////
//
// ScriptFile.cs
//
// Copyright (c) 2012 Dan Pike.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation files
// (the "Software"), to deal in the Software without restriction,
// including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software,
// and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
using Almanac;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Interfaces;
using Utilities;
using System.Configuration;
using System.Drawing;
using System.Diagnostics;

namespace ScriptConfig
{
   public class ScriptFile : IScriptFile
   {
      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// 
      /// </summary>
      private static Logger logger_;
      private string displayName_;
      private string filename_;
      private Config.Body scriptBody_;
      private Dictionary<string, IMaterialFact> undefinedMaterials_;
      private Dictionary<string, IProducerFact> undefinedProducers_;

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// 
      /// </summary>
      public ScriptFile()
      {
         logger_ = Program.Logger;
         undefinedMaterials_ = new Dictionary<string, IMaterialFact>();
         undefinedProducers_ = new Dictionary<string, IProducerFact>();
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Implementation of IScriptFile
      /// </summary>
      public string DisplayName
      {
         get { return displayName_; }
         set { displayName_ = value; }
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Implementation of IScriptFile
      /// </summary>
      public string Filename
      {
         get { return filename_; }
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Implementation of IScriptFile
      /// </summary>
      public bool Compile(string scriptFilename)
      {
         Stopwatch sw = new Stopwatch();
         sw.Start();
         try
         {
            ExeConfigurationFileMap configFileMap = new ExeConfigurationFileMap();
            configFileMap.ExeConfigFilename = scriptFilename;
            Configuration config = ConfigurationManager.OpenMappedExeConfiguration(configFileMap, ConfigurationUserLevel.None);
            if (null == config)
            {
               logger_.ErrorFormat("failed to load ScriptConfig script file '{0}'", Filename);
               return false;
            }
            if (null == (scriptBody_ = config.GetSection(Config.Body.CONFIG_SECTION) as Config.Body))
            {
               logger_.ErrorFormat("failed to parse ScriptConfig file '{0}'", Filename);
               return false;
            }
            filename_ = scriptFilename;
            logger_.DebugFormat("compiled ScriptConfig file '{0}'", Filename);
            return true;
         }
         catch (System.Exception ex)
         {
            logger_.Exception(ex);
         }
         finally
         {
            logger_.DebugFormat("loading script took {0}ms", sw.ElapsedMilliseconds);
         }
         return false;
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Implementation of IScriptFile
      /// </summary>
      public bool Initialize()
      {
         Stopwatch sw = new Stopwatch();
         sw.Start();
         try
         {
            // The system is ready to process the static data in the script
            if (!scriptBody_.MaterialDefinitions.Value.OfType<Config.MaterialDefinition>().ToList().All(config => CreateMaterial(config))
               || !scriptBody_.ProducerDefinitions.Value.OfType<Config.ProducerDefinition>().ToList().All(config => CreateProducer(config))
               || !scriptBody_.Planets.Value.OfType<Config.Planet>().ToList().All(config => CreatePlanet(config)))
            {
               logger_.ErrorFormat("failed to initialize script file '{0}'",
                  Filename);
               return false;
            }

            logger_.DebugFormat("script file '{0}' has initialized the Almanac", Filename);
            return true;
         }
         catch (System.Exception ex)
         {
            logger_.Exception(ex);
         }
         finally
         {
            logger_.DebugFormat("compiling script took {0}ms", sw.ElapsedMilliseconds);
         }
         return false;
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Implementation of IScriptFile
      /// </summary>
      public bool Run(object runner, string methodName, object[] parameters)
      {
         return false;
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Define a planet from the data in the script file
      /// </summary>
      /// <param name="configPlanet"></param>
      /// <returns></returns>
      private bool CreatePlanet(Config.Planet configPlanet)
      {
         try
         {
            IPlanetFact planet = Program.Loader.Create<IPlanetFact>();
            planet.DisplayName = configPlanet.Name;
            planet.HomeAddress = configPlanet.HomeAddress.Value;

            Program.Almanac.Planets.Add(planet);
            return true;
         }
         catch (System.Exception ex)
         {
            logger_.Exception(ex);
         }
         return false;
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Get the IMaterialFact with the given name or create a temporary
      /// object if we haven't, yet, encountered the definition of this material
      /// in the script
      /// </summary>
      /// <param name="configMaterial"></param>
      /// <returns></returns>
      private IMaterialFact AddMaterial(string name)
      {
         IMaterialFact material = Program.Almanac.Materials.FirstOrDefault(q => q.DisplayName == name);
         if (null == material)
         {
            // Do we already have a temporary object for it?
            if (!undefinedMaterials_.TryGetValue(name, out material))
            {
               // Create one and we will fill in the details later
               material = Program.Loader.Create<IMaterialFact>();
               material.DisplayName = name;
               undefinedMaterials_.Add(name, material);
            }
         }
         return material;
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Define a material from the data in the script file
      /// </summary>
      /// <param name="configMaterial"></param>
      /// <returns></returns>
      private bool CreateMaterial(Config.MaterialDefinition configMaterial)
      {
         try
         {
            IMaterialFact material = AddMaterial(configMaterial.Name);
            material.DisplayName = configMaterial.Name;
            material.Size = new XY(configMaterial.XY.Value.X, configMaterial.XY.Value.Y);

            undefinedMaterials_.Remove(configMaterial.Name);   // If this throws an exception, then we have a duplicate definition for this material
            Program.Almanac.Materials.Add(material);
            return true;
         }
         catch (System.Exception ex)
         {
            logger_.Exception(ex);
         }
         return false;
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Get the IProducerFact with the given name or create a temporary
      /// object if we haven't, yet, encountered the definition of this producer
      /// in the script
      /// </summary>
      /// <param name="configProducer"></param>
      /// <returns></returns>
      private IProducerFact AddProducer(string name)
      {
         IProducerFact producer = Program.Almanac.Producers.FirstOrDefault(q => q.DisplayName == name);
         if (null == producer)
         {
            // Do we already have a temporary object for it?
            if (!undefinedProducers_.TryGetValue(name, out producer))
            {
               // Create one and we will fill in the details later
               producer = Program.Loader.Create<IProducerFact>();
               producer.DisplayName = name;
               undefinedProducers_.Add(name, producer);
            }
         }
         return producer;
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Define a producer from the data in the script file
      /// </summary>
      /// <param name="configProducer"></param>
      /// <returns></returns>
      private bool CreateProducer(Config.ProducerDefinition configProducer)
      {
         try
         {
            IProducerFact producer = AddProducer(configProducer.Name);
            producer.DisplayName = configProducer.Name;
            producer.Size = new XY(configProducer.XY.Value.X, configProducer.XY.Value.Y);

            configProducer.Inputs.Value.OfType<Config.Product>().ToList()
               .ForEach(product => producer.SetInputRate(AddMaterial(product.Name), product.Rate));
            configProducer.Outputs.Value.OfType<Config.Product>().ToList()
               .ForEach(product => producer.SetOutputRate(AddMaterial(product.Name), product.Rate));

            undefinedProducers_.Remove(configProducer.Name);   // If this throws an exception, then we have a duplicate definition for this material
            Program.Almanac.Producers.Add(producer);
            return true;
         }
         catch (System.Exception ex)
         {
            logger_.Exception(ex);
         }
         return false;
      }
   }
}
